/*
 * MODULE:      midi.c
 *
 * DESCRIPTION: MIDI-handling code for MIDI-I/O.  Parses the MIDI-output stream and
 *   sends system-exclusive MIDI-messages out using midiOutLongMsg(), while using
 *   midiOutShortMsg() for normal MIDI-messages.  MIDI-input is passed byte-for-byte
 *   up to the Amiga's serial-port.
 *
 * AUTHOR:      Brian_King@CodePoet.com
 *
 * COPYRIGHT:   Copyright 1999, 2000 under GNU Public License
 *
 * HISTORY:
 *   1999.09.05  1.0    Brian King      - creation
 *   2000.01.30  1.1    Brian King      - addition of midi-input
 *   2002.05.xx  1.2    Bernd Roesch    - sysex in/MTC/Song Position pointer add
 */

#include "sysconfig.h"
#include "parser.h"
#include "midi.h"
#include "win32gui.h"

#if MIDI_DEBUG_DEF
    #define TRACE_MIDI(x) do { Logger::Write x; } while (0)
#else
    #define TRACE_MIDI(x)
#endif

#define MIDI_INBUFFERS 100  //use 13 MB Buffer with this settings
// on my system it work ok with 10 but who
// know when windows rest for a while
// with sysex size of 40 win can 8 sec sleep
#define INBUFFLEN 18000       //if this is not enough a warning come
int midi_inbuflen = INBUFFLEN;

static int overflow, only_one_time;
BOOL midi_ready = FALSE;
BOOL midi_in_ready = FALSE;
extern int serdev;

static HMIDIIN inHandle;
static MIDIHDR midiin[MIDI_INBUFFERS];

static byte* inbuffer[MIDI_INBUFFERS] = { 0, 0 };
static long inbufferlength[MIDI_INBUFFERS] = { 0, 0 };

static int in_allocated = 0;

/* MIDI OUTPUT */

static MidiOutStatus out_status;
static HMIDIOUT outHandle;
static MIDIHDR midiout[MIDI_BUFFERS];

static byte* outbuffer[MIDI_BUFFERS] = { 0, 0 };
static long outbufferlength[MIDI_BUFFERS] = { 0, 0 };
static int outbufferselect = 0;
static int out_allocated = 0;
static volatile int exitin = 0;
static CRITICAL_SECTION cs_proc;
/*
 * FUNCTION:   getmidiouterr
 *
 * PURPOSE:    Wrapper for midiOutGetErrorText()
 *
 * PARAMETERS:
 *   err       Midi-out error number
 *
 * RETURNS:
 *   MidiOutErrorMsg char-ptr
 *
 * NOTES:      none
 *
 * HISTORY:
 *   1999.09.06  1.0    Brian King             - Creation
 *
 */
static TCHAR* getmidiouterr(TCHAR* txt, int err)
{
    midiOutGetErrorText(err, txt, MAX_DPATH);
    return txt;
}

/*
 * FUNCTION:   MidiOut_Alloc
 *
 * PURPOSE:    Allocate the double-buffering needed for processing midi-out messages
 *
 * PARAMETERS: none
 *
 * RETURNS:
 *   allocated - the number of buffers allocated
 *
 * NOTES:      none
 *
 * HISTORY:
 *   1999.09.06  1.0    Brian King             - Creation
 *
 */
static int MidiOut_Alloc()
{
    int i;

    if (!out_allocated)
    {
        for (i = 0; i < MIDI_BUFFERS; i++)
        {
            if (!outbuffer[i])
            {
                outbuffer[i] = xmalloc(byte, BUFFLEN);
                if (outbuffer[i])
                {
                    outbufferlength[i] = BUFFLEN;
                    out_allocated++;
                }
                else
                {
                    outbufferlength[i] = 0;
                }
            }
        }
        outbufferselect = 0;
    }
    else
    {
        Logger::Write(L"MIDI: ERROR - MidiOutAlloc() called twice?\n");
    }
    return out_allocated;
}

/*
 * FUNCTION:   MidiOut_Free
 *
 * PURPOSE:    Free the double-buffering needed for processing midi-out messages
 *
 * PARAMETERS: none
 *
 * RETURNS:    none
 *
 * NOTES:      none
 *
 * HISTORY:
 *   1999.09.06  1.0    Brian King             - Creation
 *
 */
static void MidiOut_Free()
{
    int i;

    for (i = 0; i < out_allocated; i++)
    {
        if (outbuffer[i])
        {
            free(outbuffer[i]);
            outbufferlength[i] = 0;
            outbuffer[i] = nullptr;
        }
    }
    outbufferselect = 0;
    out_allocated = 0;
}

/*
 * FUNCTION:   MidiOut_PrepareHeader
 *
 * PURPOSE:    Wrapper for midiOutPrepareHeader
 *
 * PARAMETERS:
 *   midihdr   - MIDIHDR-ptr which we will prepare
 *   data      - byte-ptr to data which MIDIHDR will contain
 *   length    - length of data
 *
 * RETURNS:
 *   result    - 1 for success, 0 otherwise
 *
 * NOTES:      none
 *
 * HISTORY:
 *   1999.08.02  1.0    Brian King             - Creation
 *
 */
static int MidiOut_PrepareHeader(LPMIDIHDR out, LPSTR data, DWORD length)
{
    int result = 1;
    TCHAR err[MAX_DPATH];

    out->lpData = data;
    out->dwBufferLength = length;
    out->dwBytesRecorded = length;
    out->dwUser = 0;
    out->dwFlags = 0;

    if ((result = midiOutPrepareHeader(outHandle, out, sizeof(MIDIHDR))))
    {
        Logger::Write(L"MIDI: error %s / %d\n", getmidiouterr(err, result), result);
        result = 0;
    }
    return result;
}

/* MIDI INPUT */

/*
 * FUNCTION:   getmidiinerr
 *
 * PURPOSE:    Wrapper for midiInGetErrorText()
 *
 * PARAMETERS:
 *   err       Midi-in error number
 *
 * RETURNS:
 *   MidiInErrorMsg char-ptr
 *
 * NOTES:      none
 *
 * HISTORY:
 *   1999.09.06  1.0    Brian King             - Creation
 *
 */
static TCHAR* getmidiinerr(TCHAR* txt, int err)
{
    midiInGetErrorText(err, txt, MAX_DPATH);
    return txt;
}

/*
 * FUNCTION:   MidiIn_Alloc
 *
 * PURPOSE:    Allocate the double-buffering needed for processing midi-out messages
 *
 * PARAMETERS: none
 *
 * RETURNS:
 *   allocated - the number of buffers allocated
 *
 * NOTES:      none
 *
 * HISTORY:
 *   1999.09.06  1.0    Brian King             - Creation
 *
 */
static int MidiIn_Alloc()
{
    int i;

    if (!in_allocated)
    {
        for (i = 0; i < MIDI_INBUFFERS; i++)
        {
            if (!inbuffer[i])
            {
                inbuffer[i] = xmalloc(byte, midi_inbuflen);
                if (inbuffer[i])
                {
                    inbufferlength[i] = midi_inbuflen;
                    in_allocated++;
                }
                else
                {
                    inbufferlength[i] = 0;
                }
            }
        }
    }
    else
    {
        Logger::Write(L"MIDI: ERROR - MidiInAlloc() called twice?\n");
    }
    return in_allocated;
}

/*
 * FUNCTION:   MidiIn_Free
 *
 * PURPOSE:    Free the double-buffering needed for processing midi-in messages
 *
 * PARAMETERS: none
 *
 * RETURNS:    none
 *
 * NOTES:      none
 *
 * HISTORY:
 *   1999.09.06  1.0    Brian King             - Creation
 *
 */
static void MidiIn_Free()
{
    int i;

    for (i = 0; i < in_allocated; i++)
    {
        if (inbuffer[i])
        {
            // in_allocated--;
            free(inbuffer[i]);
            inbufferlength[i] = 0;
            inbuffer[i] = nullptr;
        }
    }
    in_allocated = 0;
    only_one_time = 0;
}

static byte plen[128] = {
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    1, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};

#define DBGOUT_MIDI_BYTE(x) TRACE_MIDI((L"MIDI: 0x%02x\n", (x)))

/*
 * FUNCTION:   Midi_Parse
 *
 * PURPOSE:    Parse the MIDI data-stream based on the direction.
 *
 * PARAMETERS:
 *   direction - Either midi_input or midi_output
 *   data      - byte of MIDI data
 *
 * RETURNS:    none
 *
 * NOTES:      Written with much assistance from Todor Fay of Blue Ribbon
 *             (Bars'n'Pipes) fame.
 *
 * HISTORY:
 *   1999.08.02  1.0    Brian King             - Creation
 *
 */
int Midi_Parse(midi_direction_e direction, BYTE* dataptr)
{
    int result = 0;
    static unsigned short bufferindex;
    static byte* bufferpoint = 0;

    if (direction == midi_output)
    {
        BYTE data = *dataptr;
        DBGOUT_MIDI_BYTE(data);
        if (data >= 0x80)
        {
            if (data >= MIDI_CLOCK)
            {
                switch (data)
                {
                    case MIDI_CLOCK:
                        TRACE_MIDI((L"MIDI: MIDI_CLOCK\n"));
                        break;
                    case MIDI_START:
                        TRACE_MIDI((L"MIDI: MIDI_START\n"));
                        break;
                    case MIDI_CONTINUE:
                        TRACE_MIDI((L"MIDI: MIDI_CONTINUE\n"));
                        break;
                    case MIDI_STOP:
                        TRACE_MIDI((L"MIDI: MIDI_STOP\n"));
                        break;
                    default:
                        break;
                }
            }
            else if (out_status.sysex)
            {
                if (out_allocated)
                {
                    bufferpoint[bufferindex++] = MIDI_EOX;
                    if (bufferindex >= BUFFLEN)
                        bufferindex = BUFFLEN - 1;
                    out_status.status = MIDI_SYSX;
                    //  Flush this buffer using midiOutLongMsg
                    MidiOut_PrepareHeader(&midiout[outbufferselect], (LPSTR)bufferpoint, bufferindex);
                    midiOutLongMsg(outHandle, &midiout[outbufferselect], sizeof(MIDIHDR));
                    outbufferselect = !outbufferselect;
                    bufferpoint = outbuffer[outbufferselect];
                    midiOutUnprepareHeader(outHandle, &midiout[outbufferselect], sizeof(MIDIHDR));
                }
                out_status.sysex = 0; // turn off MIDI_SYSX mode
                out_status.unknown = 1; // now in an unknown state
                if (data == MIDI_EOX)
                    return 0;
            }
            out_status.status = data;
            out_status.length = plen[data & 0x7F];
            out_status.posn = 0;
            out_status.unknown = 0;
            if (data == MIDI_SYSX)
            {
                out_status.sysex = 1; // turn on MIDI_SYSX mode
                if (out_allocated)
                {
                    bufferindex = 0;
                    bufferpoint = outbuffer[outbufferselect];
                    bufferpoint[bufferindex++] = MIDI_SYSX;
                }
                return 0;
            }
        }
        else if (out_status.sysex)
        {
            if (out_allocated)
            {
                bufferpoint[bufferindex++] = data;
                if (bufferindex >= BUFFLEN)
                    bufferindex = BUFFLEN - 1;
            }
            return 0;
        }
        else if (out_status.unknown)
        {
            return 0;
        }
        else if (++out_status.posn == 1)
        {
            out_status.byte1 = data;
        }
        else
        {
            out_status.byte2 = data;
        }
        if (out_status.posn >= out_status.length)
        {
            DWORD shortMsg;
            out_status.posn = 0;
            //  flush the packet using midiOutShortMessage
            shortMsg = MAKELONG(MAKEWORD(out_status.status, out_status.byte1), MAKEWORD(out_status.byte2, 0));
            midiOutShortMsg(outHandle, shortMsg);
        }
    }
    else     // handle input-data
    {
    }
    return result;
}

/*
 * FUNCTION:   MidiIn support and Callback function
 *
 * PURPOSE:    Translate Midi in messages to raw serial data

 * NOTES:      Sysex in not supported

 */

static byte midibuf[BUFFLEN];
static long midi_inptr = 0, midi_inlast = 0;

static void add1byte(DWORD_PTR w) //put 1 Byte to Midibuffer
{
    if (midi_inlast >= BUFFLEN - 10)
    {
        TRACE_MIDI((L"add1byte buffer full %d %d (%02X)\n", midi_inlast, midi_inptr, w));
        return;
    }
    midibuf[midi_inlast++] = (byte)w;
}
static void add2byte(DWORD_PTR w) //put 2 Byte to Midibuffer
{
    if (midi_inlast >= BUFFLEN - 10)
    {
        TRACE_MIDI((L"add2byte buffer full %d %d (%04X)\n", midi_inlast, midi_inptr, w));
        return;
    }
    midibuf[midi_inlast++] = (byte)w;
    w = w >> 8;
    midibuf[midi_inlast++] = (byte)w;
}
static void add3byte(DWORD_PTR w) //put 3 Byte to Midibuffer
{
    if (midi_inlast >= BUFFLEN - 10)
    {
        TRACE_MIDI((L"add3byte buffer full %d %d (%08X)\n", midi_inlast, midi_inptr, w));
        return;
    }
    midibuf[midi_inlast++] = (byte)w;
    w = w >> 8;
    midibuf[midi_inlast++] = (byte)w;
    w = w >> 8;
    midibuf[midi_inlast++] = (byte)w;
}

int ismidibyte()
{
    if (midi_inptr < midi_inlast)
        return 1;
    return 0;
}

LONG getmidibyte() //return midibyte or -1 if none
{
    int i;
    LONG rv;

    EnterCriticalSection(&cs_proc);
    if (overflow == 1)
    {
        TCHAR szMessage[MAX_DPATH];
        WIN32GUI_LoadUIString(IDS_MIDIOVERFLOW, szMessage, MAX_DPATH);
        gui_message(szMessage);
        overflow = 0;
    }
    TRACE_MIDI((L"getmidibyte(%02X)\n", midibuf[midi_inptr]));
    if (midibuf[midi_inptr] >= 0xf0)   // only check for free buffers if status sysex
    {
        for (i = 0; i < MIDI_INBUFFERS; i++)
        {
            if (midiin[i].dwFlags == (MHDR_DONE | MHDR_PREPARED))
            {
                //  add a buffer if one is free
                LeaveCriticalSection(&cs_proc);
                midiInAddBuffer(inHandle, &midiin[i], sizeof(MIDIHDR));
                EnterCriticalSection(&cs_proc);
            }
        }
    }
    rv = -1;
    if (midi_inptr < midi_inlast)
        rv = midibuf[midi_inptr++];
    if (midi_inptr >= midi_inlast)
        midi_inptr = midi_inlast = 0;
    LeaveCriticalSection(&cs_proc);
    return rv;
}

static void CALLBACK MidiInProc(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
{
    EnterCriticalSection(&cs_proc);
    if (wMsg == MIM_ERROR)
    {
        TRACE_MIDI((L"MIDI Data Lost\n"));
    }
    if (wMsg == MIM_LONGDATA)
    {
        LPMIDIHDR midiin = (LPMIDIHDR)dwParam1;
        static long synum;
        TRACE_MIDI((L"MIM_LONGDATA bytes=%d ts=%u\n", midiin->dwBytesRecorded, dwParam2));
        if (exitin == 1)
            goto end;  //for safeness midi want close
        if ((midi_inlast + midiin->dwBytesRecorded) >= (BUFFLEN - 6))
        {
            overflow = 1;
            TRACE_MIDI((L"MIDI overflow1\n"));
            // for safeness if buffer too full (should not occur)
            goto end;
        }
        if (midiin->dwBufferLength == midiin->dwBytesRecorded)
        {
            // for safeness if buffer too full (should not occur)
            overflow = 1;
            TRACE_MIDI((L"MIDI overflow2\n"));
            goto end;
        }
        memcpy(&midibuf[midi_inlast], midiin->lpData, midiin->dwBytesRecorded);
        midi_inlast = midi_inlast + midiin->dwBytesRecorded;
    }

    if (wMsg == MM_MIM_DATA || wMsg == MM_MIM_MOREDATA)
    {
        BYTE state = (BYTE)dwParam1;
        TRACE_MIDI((L"%s %08X\n", wMsg == MM_MIM_DATA ? "MM_MIM_DATA" : "MM_MIM_MOREDATA", dwParam1));
        if (state == 254)
            goto end;
        if (state < 0xf0)
            state = state & 0xf0;
        switch (state)
        {
            case 0x80: //Note OFF
                add3byte(dwParam1);
                break;
            case 0x90: // Note On
                add3byte(dwParam1);
                break;
            case 0xa0: // Poly Press
                add3byte(dwParam1);
                break;
            case 0xb0: //CTRL Change
                add3byte(dwParam1);
                break;
            case 0xc0: //ProgramChange
                add2byte(dwParam1);
                break;
            case 0xd0: //ChanPress
                add2byte(dwParam1);
                break;
            case 0xe0: //PitchBend
                add3byte(dwParam1);
                break;
            // System Common Messages
            case 0xf1: //QuarterFrame-message ... MIDI_Time_Code
                add2byte(dwParam1);
                break;
            case 0xf2: //Song Position
                add3byte(dwParam1);
                break;
            case 0xf3: //Song Select
                add2byte(dwParam1);
                break;
            case 0xf6: //Tune Request
                add3byte(dwParam1);
                break;
            // System Real Time Messages
            case 0xf8: //MIDI-Clock
                add1byte((char)dwParam1);
                break;
            case 0xfa: //Start
                add1byte((char)dwParam1);
                break;
            case 0xfb: //Continue
                add1byte((char)dwParam1);
                break;
            case 0xfc: //Stop
                add1byte((char)dwParam1);
                break;
            case 0xfe: //Active Sense (selden used)
                add1byte((char)dwParam1);
                break;
            case 0xff: //Reset (selden used)
                add2byte(dwParam1);
                break;
        }
    }
end:
    LeaveCriticalSection(&cs_proc);
}
/*
 * FUNCTION:   Midi_Open
 *
 * PURPOSE:    Open the desired MIDI handles (output, and possibly input)
 *
 * PARAMETERS: none
 *
 * RETURNS:
 *   result    - 1 for success, 0 for failure
 *
 * NOTES:      none
 *
 * HISTORY:
 *   1999.08.02  1.0    Brian King             - Creation
 *
 */
int Midi_Open()
{
    uint result = 0, i;
    TCHAR err[MAX_DPATH];

    if ((result = midiOutOpen(&outHandle, g_curr_conf.win32_midioutdev, 0, 0, CALLBACK_NULL)))
    {
        Logger::Write(L"MIDI OUT: error %s / %d while opening port %d\n", getmidiouterr(err, result), result, g_curr_conf.win32_midioutdev);
        result = 0;
    }
    else
    {
        InitializeCriticalSection(&cs_proc);
        //  We don't need input for output...
        if ((g_curr_conf.win32_midiindev >= 0) &&
            (result = midiInOpen(&inHandle, g_curr_conf.win32_midiindev, (DWORD_PTR)MidiInProc, 0, CALLBACK_FUNCTION | MIDI_IO_STATUS)))
        {
            Logger::Write(L"MIDI IN: error %s / %d while opening port %d\n", getmidiinerr(err, result), result, g_curr_conf.win32_midiindev);
        }
        else
        {
            midi_in_ready = TRUE;
            result = midiInStart(inHandle);
        }

        if (MidiOut_Alloc())
        {
            if (midi_in_ready)
            {
                if (!MidiIn_Alloc())
                {
                    midiInClose(inHandle);
                    midi_in_ready = FALSE;
                }
                else
                {
                    for (i = 0; i < MIDI_INBUFFERS; i++)
                    {
                        midiin[i].lpData = (LPSTR)inbuffer[i];
                        midiin[i].dwBufferLength = midi_inbuflen - 1;
                        midiin[i].dwBytesRecorded = midi_inbuflen - 1;
                        midiin[i].dwUser = 0;
                        midiin[i].dwFlags = 0;
                        result = midiInPrepareHeader(inHandle, &midiin[i], sizeof(MIDIHDR));
                        result = midiInAddBuffer(inHandle, &midiin[i], sizeof(MIDIHDR));
                    }
                }
            }
            midi_ready = TRUE;
            result = 1;
            serdev = 1;
        }
        else
        {
            midiOutClose(outHandle);
            if (midi_in_ready)
            {
                midiInClose(inHandle);
                midi_in_ready = FALSE;
            }
            result = 0;
            DeleteCriticalSection(&cs_proc);
        }
    }
    return result;
}

/*
 * FUNCTION:   Midi_Close
 *
 * PURPOSE:    Close all opened MIDI handles
 *
 * PARAMETERS: none
 *
 * RETURNS:    none
 *
 * NOTES:      none
 *
 * HISTORY:
 *   1999.08.02  1.0    Brian King             - Creation
 *
 */
void Midi_Close()
{
    int i;
    if (midi_ready)
    {
        midiOutReset(outHandle);
        for (i = 0; i < MIDI_BUFFERS; i++)
        {
            while (MIDIERR_STILLPLAYING == midiOutUnprepareHeader(outHandle, &midiout[i], sizeof(MIDIHDR)))
            {
                Sleep(10);
            }
        }
        MidiOut_Free();
        midiOutClose(outHandle);

        if (midi_in_ready)
        {
            exitin = 1; //for safeness sure no callback come now
            midiInReset(inHandle);
            for (i = 0; i < MIDI_INBUFFERS; i++)
            {
                midiInUnprepareHeader(inHandle, &midiin[i], sizeof(MIDIHDR));
            }
            MidiIn_Free();
            midiInClose(inHandle);
            midi_in_ready = FALSE;
            exitin = 0;
        }
        midi_ready = FALSE;
        Logger::Write(L"MIDI: closed.\n");
        DeleteCriticalSection(&cs_proc);
    }
}